
package com.jf.cloud.seckill.service.impl;

import com.alibaba.nacos.common.utils.CollectionUtils;
import com.jf.cloud.api.order.feign.OrderFeignClient;
import com.jf.cloud.common.cache.constant.CacheNames;
import com.jf.cloud.common.cache.constant.LockCacheNames;
import com.jf.cloud.common.cache.constant.SeckillCacheNames;
import com.jf.cloud.common.cache.util.CacheManagerUtil;
import com.jf.cloud.common.cache.util.RedisUtil;
import com.jf.cloud.common.exception.LuckException;
import com.jf.cloud.common.order.constant.OrderActivityType;
import com.jf.cloud.seckill.constant.SeckillConstant;
import com.jf.cloud.seckill.dto.SeckillSkuDTO;
import com.jf.cloud.seckill.mapper.SeckillMapper;
import com.jf.cloud.seckill.mapper.SeckillSkuMapper;
import com.jf.cloud.seckill.service.SeckillSkuService;
import com.jf.cloud.seckill.vo.SeckillSkuVO;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;

/**
 * 秒杀活动sku
 *
 * @author zz
 * @date 2021-03-30 14:53:09
 */
@Service
public class SeckillSkuServiceImpl implements SeckillSkuService {

    @Autowired
    private SeckillSkuMapper seckillSkuMapper;

    @Autowired
    private CacheManagerUtil cacheManagerUtil;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private OrderFeignClient orderFeignClient;

    @Autowired
    private SeckillMapper seckillMapper;

    @Override
    @Cacheable(cacheNames = SeckillCacheNames.SECKILL_SKU_BY_ID, key = "#seckillSkuId", sync = true)
    public SeckillSkuVO getBySeckillSkuId(Long seckillSkuId) {
        // 这里的库存不准的不要用这个方法获取库存，请使用getStockBySeckillSkuId 这个方法获取库存
        return seckillSkuMapper.getBySeckillSkuId(seckillSkuId);
    }

    @Override
    @Cacheable(cacheNames = SeckillCacheNames.SECKILL_SKU_BY_SECKILL_ID, key = "#seckillId", sync = true)
    public List<SeckillSkuVO> listSeckillSkuBySeckillId(Long seckillId) {
        return seckillSkuMapper.selectListBySeckillId(seckillId);
    }

    @Override
    public void saveBatch(List<SeckillSkuDTO> seckillSkuList) {
        seckillSkuMapper.saveBatch(seckillSkuList);
        // 库存分片到redis
        for (SeckillSkuDTO seckillSku : seckillSkuList) {
            doZone(seckillSku.getSeckillSkuId(), seckillSku.getSeckillStocks());
        }
    }

    @Override
    @CacheEvict(cacheNames = SeckillCacheNames.SECKILL_SKU_BY_SECKILL_ID, key = "#seckillId")
    public void removeSeckillSkuCacheBySeckillId(Long seckillId) {
        List<SeckillSkuVO> seckillSkus = listSeckillSkuBySeckillId(seckillId);
        //清除缓存
        if (CollectionUtils.isNotEmpty(seckillSkus)) {
            for (SeckillSkuVO seckillSku : seckillSkus) {
                // 不能采用this清除缓存，因为这样没有aop
                cacheManagerUtil.evictCache(SeckillCacheNames.SECKILL_SKU_BY_ID, String.valueOf(seckillSku.getSeckillSkuId()));
            }
        }
    }

    @Override
    public Integer doZone(Long seckillSkuId, Integer totalStock) {
        // 分片数量
        int zoneNum = SeckillConstant.ZONE_NUM;
        // 可用分片数量
        int availableZone = 0;
        if (totalStock == 0) {
            RedisUtil.set(SeckillCacheNames.SECKILL_SKU_AVAILABLE_ZONE + seckillSkuId, availableZone, 0);
            // 分片库存数量归零
            for (int i = 0; i < zoneNum; i++) {
               RedisUtil.set(SeckillCacheNames.SECKILL_SKU_STOCK + seckillSkuId + CacheNames.UNION_KEY + i, 0, 0);
            }
            return availableZone;
        }

        // 每个分片至少的库存数量（目前是想分片出来的结果要么是这个数量，要么是0）
        // 比如 ( 10,10,5,0,0,0,0,) 这种结果
        int avgStock = getAvgStock(zoneNum, totalStock);
        // 获取要存进缓存的每个分片库存数量
        for (int i = 0; i < zoneNum; i++) {

            if (totalStock != 0) {
                availableZone = i + 1;
            }
            // 更新缓存
            if (totalStock - avgStock >= 0) {
                RedisUtil.set(SeckillCacheNames.SECKILL_SKU_STOCK + seckillSkuId + CacheNames.UNION_KEY + i, avgStock, 0);
                totalStock -= avgStock;
            } else {
                RedisUtil.set(SeckillCacheNames.SECKILL_SKU_STOCK + seckillSkuId + CacheNames.UNION_KEY + i, totalStock, 0);
                totalStock = 0;
            }
        }
        // 缓存可用分片数量
        RedisUtil.set(SeckillCacheNames.SECKILL_SKU_AVAILABLE_ZONE + seckillSkuId, availableZone, 0);
        return availableZone;
    }

    @Override
    public Integer getStockBySeckillSkuId(Long seckillSkuId) {
        int totalStock = 0;
        RLock rLock = redissonClient.getLock(LockCacheNames.LOCK_SECKILL_ZONE_PERFIX + seckillSkuId);
        try {
            rLock.lock();
            // 遍历redis
            for (int i = 0; i < SeckillConstant.ZONE_NUM; i++) {
                Integer zoneStock = RedisUtil.get(SeckillCacheNames.SECKILL_SKU_STOCK + seckillSkuId + CacheNames.UNION_KEY + i);
                // 缓存查不到，就通过数据库或者订单进行查询
                if (zoneStock == null) {
                    // 通过数据库获取库存数量，这里的库存数量不一定准确的
                    SeckillSkuVO skuInfo = seckillSkuMapper.getBySeckillSkuId(seckillSkuId);

                    // 总库存量，需要查一下销量，才知道到底还有多少库存
                    Integer salesNum = orderFeignClient.countActivitySkuSale(skuInfo.getSkuId(), skuInfo.getSeckillId(), OrderActivityType.SECKILL.value()).getData();

                    // 剩余库存
                    int stockByOrder = skuInfo.getSeckillOriginStocks() - salesNum;

                    // 看下通过订单数出来的库存，和原始库存那个比较少，那个少哪个就最接近“真实库存”。
                    // 所谓的接近真实都是建立在缓存被删了，然后没办法恢复的状态，只能通过订单和历史库存进行查询的结果。
                    // 正常情况下，缓存的库存就是真实库存。
                    return Math.min(skuInfo.getSeckillStocks(), stockByOrder);
                } else {
                    totalStock += zoneStock;
                }
            }
        } finally {
            rLock.unlock();
        }
        return totalStock;
    }

    @Override
    public Integer getStockBySeckillId(Long seckillId) {
        SeckillSkuServiceImpl seckillSkuService = (SeckillSkuServiceImpl) AopContext.currentProxy();
        List<SeckillSkuVO> skuList = seckillSkuService.listSeckillSkuBySeckillId(seckillId);
        List<Long> skuIds = skuList.stream().map(SeckillSkuVO::getSeckillSkuId).collect(Collectors.toList());
        return skuIds.stream().map(this::getStockBySeckillSkuId).mapToInt(stock -> stock).sum();
    }

    @Override
    public Integer getAvailableZone(Long seckillSkuId, Long skuId, Long activityId) {
        Integer availableZone = RedisUtil.get(SeckillCacheNames.SECKILL_SKU_AVAILABLE_ZONE + seckillSkuId);
        // 如果没有分片的数据，说明缓存还没有创建/缓存被删除了。
        if (availableZone != null) {
            return availableZone;
        }
        // 可用库存
        Integer stock = getStockBySeckillSkuId(seckillSkuId);
        // 重新构建缓存
        return doZone(seckillSkuId, stock);
    }

    @Override
    public List<SeckillSkuVO> listMaybeNeedStockPlacementActivity() {
        return seckillSkuMapper.listMaybeNeedStockPlacementActivity();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void flushStockToDb(Long seckillSkuId, Long seckillId) {
        // 可用库存
        Integer hasStock = getStockBySeckillSkuId(seckillSkuId);

        SeckillSkuVO seckillSku = seckillSkuMapper.getBySeckillSkuId(seckillSkuId);
        // 数据库里面的还没更新的剩余库存
        Integer seckillStocks = seckillSku.getSeckillStocks();
        // 改变的库存
        Integer changeStock = seckillStocks - hasStock;
        // 数据就真的没有变过，不需要更新数据库
        if (changeStock == 0) {
            return;
        }
        // 更新数据库秒杀活动的剩余库存
        seckillMapper.updateStocksById(seckillId, changeStock);
        // 更新数据库sku剩余的库存
        seckillSkuMapper.updateStocks(seckillSkuId, changeStock);
    }

    @Override
    public List<SeckillSkuVO> listOverJustNow() {
        return seckillSkuMapper.listOverJustNow();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void rollbackSecKillStock(Long activityId, Long skuId, Integer count) {
        SeckillSkuVO secKillSku = seckillSkuMapper.getBySkuIdAndSeckillId(activityId, skuId);
        if (Objects.isNull(secKillSku)) {
            throw new LuckException("秒杀商品信息不存在");
        }
        // 还原秒杀库存(数据库)
        if (secKillSku.getSeckillStocks().compareTo(secKillSku.getSeckillOriginStocks()) < 0) {
            seckillSkuMapper.returnStocksByOrderId(secKillSku.getSeckillSkuId(), count);
            seckillMapper.returnStocksByOrderId(activityId, count);
        }
        // 还原秒杀库存(redis)
        RLock rLock = redissonClient.getLock(LockCacheNames.LOCK_SECKILL_ZONE_PERFIX + secKillSku.getSeckillSkuId());
        try {
            rLock.lock();
            Integer availableZone = this.getAvailableZone(secKillSku.getSeckillSkuId(),secKillSku.getSkuId(),secKillSku.getSeckillId());
            // 库存回退了，所以有可用的库存分片了
            if (availableZone == 0) {
                RedisUtil.set(SeckillCacheNames.SECKILL_SKU_AVAILABLE_ZONE + secKillSku.getSeckillSkuId(), 1, -1);
                availableZone = 1;
            }
            // 随机对一个分片增加库存
            Random random = new Random();
            int zoneNo = random.nextInt(availableZone);
            RedisUtil.incr(SeckillCacheNames.SECKILL_SKU_STOCK + secKillSku.getSeckillSkuId() + CacheNames.UNION_KEY + zoneNo, count);
        } finally {
            rLock.unlock();
        }
    }

    private int getAvgStock(int zoneNum, Integer totalStock) {

        // 每个分片可以分配的库存数量，可能小于10（这里的平均库存并不是真正的平均库存，因为这里进行了线上取整）
        // 比如平均库存是 333.33 可以能会变成334
        int everyZoneStockRoundUp =divToRoundUp(totalStock, zoneNum);

        if (everyZoneStockRoundUp < SeckillConstant.ZONE_MIN_STOCK) {
            return SeckillConstant.ZONE_MIN_STOCK;
        } else {
            // 平均库存向上取整，因为就算是平均库存
            return everyZoneStockRoundUp;
        }
    }

    /**
     * 两个数相除然后向上取整
     * @param a 除数
     * @param b 被除数
     * @return 返回值
     */
    private int divToRoundUp(int a, int b) {
        return new BigDecimal(a).divide(new BigDecimal(b),0, RoundingMode.UP).intValue();
    }
}
